/*
===========================================================================
Copyright (C) 1999-2005 Id Software, Inc.

This file is part of Quake III Arena source code.

Quake III Arena source code is free software; you can redistribute it
and/or modify it under the terms of the GNU General Public License as
published by the Free Software Foundation; either version 2 of the License,
or (at your option) any later version.

Quake III Arena source code is distributed in the hope that it will be
useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with Quake III Arena source code; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
===========================================================================
 */
//

/*****************************************************************************
 * name:		ai_main.c
 *
 * desc:		Quake3 bot AI
 *
 * $Archive: /MissionPack/code/game/ai_main.c $
 *
 *****************************************************************************/


#include "g_local.h"
#include "../qcommon/q_shared.h"
#include "../botlib/botlib.h"		//bot lib interface
#include "../botlib/be_aas.h"
#include "../botlib/be_ea.h"
#include "../botlib/be_ai_char.h"
#include "../botlib/be_ai_chat.h"
#include "../botlib/be_ai_gen.h"
#include "../botlib/be_ai_goal.h"
#include "../botlib/be_ai_move.h"
#include "../botlib/be_ai_weap.h"
//
#include "ai_main.h"
#include "ai_dmq3.h"
#include "ai_chat.h"
#include "ai_cmd.h"
#include "ai_dmnet.h"
#include "ai_vcmd.h"

//
#include "chars.h"
#include "inv.h"
#include "syn.h"

#ifndef MAX_PATH
#define MAX_PATH		144
#endif


//bot states
bot_state_t *botstates[MAX_CLIENTS];
//number of bots
int numbots;
//floating point time
float floattime;
//time to do a regular update
float regularupdate_time;
//
int bot_interbreed;
int bot_interbreedmatchcount;
//
vmCvar_t bot_thinktime;
vmCvar_t bot_memorydump;
vmCvar_t bot_saveroutingcache;
vmCvar_t bot_pause;
vmCvar_t bot_report;
vmCvar_t bot_testsolid;
vmCvar_t bot_testclusters;
vmCvar_t bot_developer;
vmCvar_t bot_interbreedchar;
vmCvar_t bot_interbreedbots;
vmCvar_t bot_interbreedcycle;
vmCvar_t bot_interbreedwrite;


void ExitLevel(void);

/*
==================
BotAI_Print
==================
 */
void QDECL BotAI_Print(int type, char *fmt, ...) {
    char str[2048];
    va_list ap;

    va_start(ap, fmt);
    Q_vsnprintf(str, sizeof (str), fmt, ap);
    va_end(ap);

    switch (type) {
        case PRT_MESSAGE:
        {
            G_Printf("%s", str);
            break;
        }
        case PRT_WARNING:
        {
            G_Printf(S_COLOR_YELLOW "Warning: %s", str);
            break;
        }
        case PRT_ERROR:
        {
            G_Printf(S_COLOR_RED "Error: %s", str);
            break;
        }
        case PRT_FATAL:
        {
            G_Printf(S_COLOR_RED "Fatal: %s", str);
            break;
        }
        case PRT_EXIT:
        {
            G_Error(S_COLOR_RED "Exit: %s", str);
            break;
        }
        default:
        {
            G_Printf("unknown print type\n");
            break;
        }
    }
}

/*
==================
BotAI_Trace
==================
 */
void BotAI_Trace(bsp_trace_t *bsptrace, vec3_t start, vec3_t mins, vec3_t maxs, vec3_t end, int passent, int contentmask) {
    trace_t trace;

    trap_Trace(&trace, start, mins, maxs, end, passent, contentmask);
    //copy the trace information
    bsptrace->allsolid = trace.allsolid;
    bsptrace->startsolid = trace.startsolid;
    bsptrace->fraction = trace.fraction;
    VectorCopy(trace.endpos, bsptrace->endpos);
    bsptrace->plane.dist = trace.plane.dist;
    VectorCopy(trace.plane.normal, bsptrace->plane.normal);
    bsptrace->plane.signbits = trace.plane.signbits;
    bsptrace->plane.type = trace.plane.type;
    bsptrace->surface.value = trace.surfaceFlags;
    bsptrace->ent = trace.entityNum;
    bsptrace->exp_dist = 0;
    bsptrace->sidenum = 0;
    bsptrace->contents = 0;
}

/*
==================
BotAI_GetClientState
==================
 */
int BotAI_GetClientState(int clientNum, playerState_t *state) {
    gentity_t *ent;

    ent = &g_entities[clientNum];
    if (!ent->inuse) {
        return qfalse;
    }
    if (!ent->client) {
        return qfalse;
    }

    memcpy(state, &ent->client->ps, sizeof (playerState_t));
    return qtrue;
}

/*
==================
BotAI_GetEntityState
==================
 */
int BotAI_GetEntityState(int entityNum, entityState_t *state) {
    gentity_t *ent;

    ent = &g_entities[entityNum];
    memset(state, 0, sizeof (entityState_t));
    if (!ent->inuse) return qfalse;
    if (!ent->r.linked) return qfalse;
    if (ent->r.svFlags & SVF_NOCLIENT) return qfalse;
    memcpy(state, &ent->s, sizeof (entityState_t));
    return qtrue;
}

/*
==================
BotAI_GetSnapshotEntity
==================
 */
int BotAI_GetSnapshotEntity(int clientNum, int sequence, entityState_t *state) {
    int entNum;

    entNum = trap_BotGetSnapshotEntity(clientNum, sequence);
    if (entNum == -1) {
        memset(state, 0, sizeof (entityState_t));
        return -1;
    }

    BotAI_GetEntityState(entNum, state);

    return sequence + 1;
}

/*
==================
BotAI_BotInitialChat
==================
 */
void QDECL BotAI_BotInitialChat(bot_state_t *bs, char *type, ...) {
    int i, mcontext;
    va_list ap;
    char *p;
    char *vars[MAX_MATCHVARIABLES];

    memset(vars, 0, sizeof (vars));
    va_start(ap, type);
    p = va_arg(ap, char *);
    for (i = 0; i < MAX_MATCHVARIABLES; i++) {
        if (!p) {
            break;
        }
        vars[i] = p;
        p = va_arg(ap, char *);
    }
    va_end(ap);

    mcontext = BotSynonymContext(bs);

    trap_BotInitialChat(bs->cs, type, mcontext, vars[0], vars[1], vars[2], vars[3], vars[4], vars[5], vars[6], vars[7]);
}

/*
==================
BotTestAAS
==================
 */
void BotTestAAS(vec3_t origin) {
    int areanum;
    aas_areainfo_t info;

    trap_Cvar_Update(&bot_testsolid);
    trap_Cvar_Update(&bot_testclusters);
    if (bot_testsolid.integer) {
        if (!trap_AAS_Initialized()) return;
        areanum = BotPointAreaNum(origin);
        if (areanum) BotAI_Print(PRT_MESSAGE, "\remtpy area");
        else BotAI_Print(PRT_MESSAGE, "\r^1SOLID area");
    } else if (bot_testclusters.integer) {
        if (!trap_AAS_Initialized()) return;
        areanum = BotPointAreaNum(origin);
        if (!areanum)
            BotAI_Print(PRT_MESSAGE, "\r^1Solid!                              ");
        else {
            trap_AAS_AreaInfo(areanum, &info);
            BotAI_Print(PRT_MESSAGE, "\rarea %d, cluster %d       ", areanum, info.cluster);
        }
    }
}

/*
==================
BotReportStatus
==================
 */
void BotReportStatus(bot_state_t *bs) {
    char goalname[MAX_MESSAGE_SIZE];
    char netname[MAX_MESSAGE_SIZE];
    char *leader, flagstatus[32];
    //
    ClientName(bs->client, netname, sizeof (netname));
    if (Q_stricmp(netname, bs->teamleader) == 0) leader = "L";
    else leader = " ";

    strcpy(flagstatus, "  ");

    switch (bs->ltgtype) {
        case LTG_TEAMHELP:
        {
            EasyClientName(bs->teammate, goalname, sizeof (goalname));
            BotAI_Print(PRT_MESSAGE, "%-20s%s%s: helping %s\n", netname, leader, flagstatus, goalname);
            break;
        }
        case LTG_TEAMACCOMPANY:
        {
            EasyClientName(bs->teammate, goalname, sizeof (goalname));
            BotAI_Print(PRT_MESSAGE, "%-20s%s%s: accompanying %s\n", netname, leader, flagstatus, goalname);
            break;
        }
        case LTG_DEFENDKEYAREA:
        {
            trap_BotGoalName(bs->teamgoal.number, goalname, sizeof (goalname));
            BotAI_Print(PRT_MESSAGE, "%-20s%s%s: defending %s\n", netname, leader, flagstatus, goalname);
            break;
        }
        case LTG_GETITEM:
        {
            trap_BotGoalName(bs->teamgoal.number, goalname, sizeof (goalname));
            BotAI_Print(PRT_MESSAGE, "%-20s%s%s: getting item %s\n", netname, leader, flagstatus, goalname);
            break;
        }
        case LTG_KILL:
        {
            ClientName(bs->teamgoal.entitynum, goalname, sizeof (goalname));
            BotAI_Print(PRT_MESSAGE, "%-20s%s%s: killing %s\n", netname, leader, flagstatus, goalname);
            break;
        }
        case LTG_CAMP:
        case LTG_CAMPORDER:
        {
            BotAI_Print(PRT_MESSAGE, "%-20s%s%s: camping\n", netname, leader, flagstatus);
            break;
        }
        case LTG_PATROL:
        {
            BotAI_Print(PRT_MESSAGE, "%-20s%s%s: patrolling\n", netname, leader, flagstatus);
            break;
        }
        case LTG_GETFLAG:
        {
            BotAI_Print(PRT_MESSAGE, "%-20s%s%s: capturing flag\n", netname, leader, flagstatus);
            break;
        }
        case LTG_RUSHBASE:
        {
            BotAI_Print(PRT_MESSAGE, "%-20s%s%s: rushing base\n", netname, leader, flagstatus);
            break;
        }
        case LTG_RETURNFLAG:
        {
            BotAI_Print(PRT_MESSAGE, "%-20s%s%s: returning flag\n", netname, leader, flagstatus);
            break;
        }
        case LTG_ATTACKENEMYBASE:
        {
            BotAI_Print(PRT_MESSAGE, "%-20s%s%s: attacking the enemy base\n", netname, leader, flagstatus);
            break;
        }
        case LTG_HARVEST:
        {
            BotAI_Print(PRT_MESSAGE, "%-20s%s%s: harvesting\n", netname, leader, flagstatus);
            break;
        }
        default:
        {
            BotAI_Print(PRT_MESSAGE, "%-20s%s%s: roaming\n", netname, leader, flagstatus);
            break;
        }
    }
}

/*
==================
BotTeamplayReport
==================
 */
void BotTeamplayReport(void) {
    int i;
    char buf[MAX_INFO_STRING];

    BotAI_Print(PRT_MESSAGE, S_COLOR_RED"RED\n");
    for (i = 0; i < maxclients && i < MAX_CLIENTS; i++) {
        //
        if (!botstates[i] || !botstates[i]->inuse) continue;
        //
        trap_GetConfigstring(CS_PLAYERS + i, buf, sizeof (buf));
        //if no config string or no name
        if (!strlen(buf) || !strlen(Info_ValueForKey(buf, "n"))) continue;
        //skip spectators
        if (atoi(Info_ValueForKey(buf, "t")) == TEAM_RED) {
            BotReportStatus(botstates[i]);
        }
    }
    BotAI_Print(PRT_MESSAGE, S_COLOR_BLUE"BLUE\n");
    for (i = 0; i < maxclients && i < MAX_CLIENTS; i++) {
        //
        if (!botstates[i] || !botstates[i]->inuse) continue;
        //
        trap_GetConfigstring(CS_PLAYERS + i, buf, sizeof (buf));
        //if no config string or no name
        if (!strlen(buf) || !strlen(Info_ValueForKey(buf, "n"))) continue;
        //skip spectators
        if (atoi(Info_ValueForKey(buf, "t")) == TEAM_BLUE) {
            BotReportStatus(botstates[i]);
        }
    }
}

/*
==================
BotSetInfoConfigString
==================
 */
void BotSetInfoConfigString(bot_state_t *bs) {
    char goalname[MAX_MESSAGE_SIZE];
    char netname[MAX_MESSAGE_SIZE];
    char action[MAX_MESSAGE_SIZE];
    char *leader, carrying[32], *cs;
    bot_goal_t goal;
    //
    ClientName(bs->client, netname, sizeof (netname));
    if (Q_stricmp(netname, bs->teamleader) == 0) leader = "L";
    else leader = " ";

    strcpy(carrying, "  ");

    switch (bs->ltgtype) {
        case LTG_TEAMHELP:
        {
            EasyClientName(bs->teammate, goalname, sizeof (goalname));
            Com_sprintf(action, sizeof (action), "helping %s", goalname);
            break;
        }
        case LTG_TEAMACCOMPANY:
        {
            EasyClientName(bs->teammate, goalname, sizeof (goalname));
            Com_sprintf(action, sizeof (action), "accompanying %s", goalname);
            break;
        }
        case LTG_DEFENDKEYAREA:
        {
            trap_BotGoalName(bs->teamgoal.number, goalname, sizeof (goalname));
            Com_sprintf(action, sizeof (action), "defending %s", goalname);
            break;
        }
        case LTG_GETITEM:
        {
            trap_BotGoalName(bs->teamgoal.number, goalname, sizeof (goalname));
            Com_sprintf(action, sizeof (action), "getting item %s", goalname);
            break;
        }
        case LTG_KILL:
        {
            ClientName(bs->teamgoal.entitynum, goalname, sizeof (goalname));
            Com_sprintf(action, sizeof (action), "killing %s", goalname);
            break;
        }
        case LTG_CAMP:
        case LTG_CAMPORDER:
        {
            Com_sprintf(action, sizeof (action), "camping");
            break;
        }
        case LTG_PATROL:
        {
            Com_sprintf(action, sizeof (action), "patrolling");
            break;
        }
        case LTG_GETFLAG:
        {
            Com_sprintf(action, sizeof (action), "capturing flag");
            break;
        }
        case LTG_RUSHBASE:
        {
            Com_sprintf(action, sizeof (action), "rushing base");
            break;
        }
        case LTG_RETURNFLAG:
        {
            Com_sprintf(action, sizeof (action), "returning flag");
            break;
        }
        case LTG_ATTACKENEMYBASE:
        {
            Com_sprintf(action, sizeof (action), "attacking the enemy base");
            break;
        }
        case LTG_HARVEST:
        {
            Com_sprintf(action, sizeof (action), "harvesting");
            break;
        }
        default:
        {
            trap_BotGetTopGoal(bs->gs, &goal);
            trap_BotGoalName(goal.number, goalname, sizeof (goalname));
            Com_sprintf(action, sizeof (action), "roaming %s", goalname);
            break;
        }
    }
    cs = va("l\\%s\\c\\%s\\a\\%s",
            leader,
            carrying,
            action);
    trap_SetConfigstring(CS_BOTINFO + bs->client, cs);
}

/*
==============
BotUpdateInfoConfigStrings
==============
 */
void BotUpdateInfoConfigStrings(void) {
    int i;
    char buf[MAX_INFO_STRING];

    for (i = 0; i < maxclients && i < MAX_CLIENTS; i++) {
        //
        if (!botstates[i] || !botstates[i]->inuse)
            continue;
        //
        trap_GetConfigstring(CS_PLAYERS + i, buf, sizeof (buf));
        //if no config string or no name
        if (!strlen(buf) || !strlen(Info_ValueForKey(buf, "n")))
            continue;
        BotSetInfoConfigString(botstates[i]);
    }
}

/*
==============
BotInterbreedBots
==============
 */
void BotInterbreedBots(void) {
    float ranks[MAX_CLIENTS];
    int parent1, parent2, child;
    int i;

    // get rankings for all the bots
    for (i = 0; i < MAX_CLIENTS; i++) {
        if (botstates[i] && botstates[i]->inuse) {
            ranks[i] = botstates[i]->num_kills * 2 - botstates[i]->num_deaths;
        } else {
            ranks[i] = -1;
        }
    }

    if (trap_GeneticParentsAndChildSelection(MAX_CLIENTS, ranks, &parent1, &parent2, &child)) {
        trap_BotInterbreedGoalFuzzyLogic(botstates[parent1]->gs, botstates[parent2]->gs, botstates[child]->gs);
        trap_BotMutateGoalFuzzyLogic(botstates[child]->gs, 1);
    }
    // reset the kills and deaths
    for (i = 0; i < MAX_CLIENTS; i++) {
        if (botstates[i] && botstates[i]->inuse) {
            botstates[i]->num_kills = 0;
            botstates[i]->num_deaths = 0;
        }
    }
}

/*
==============
BotWriteInterbreeded
==============
 */
void BotWriteInterbreeded(char *filename) {
    float rank, bestrank;
    int i, bestbot;

    bestrank = 0;
    bestbot = -1;
    // get the best bot
    for (i = 0; i < MAX_CLIENTS; i++) {
        if (botstates[i] && botstates[i]->inuse) {
            rank = botstates[i]->num_kills * 2 - botstates[i]->num_deaths;
        } else {
            rank = -1;
        }
        if (rank > bestrank) {
            bestrank = rank;
            bestbot = i;
        }
    }
    if (bestbot >= 0) {
        //write out the new goal fuzzy logic
        trap_BotSaveGoalFuzzyLogic(botstates[bestbot]->gs, filename);
    }
}

/*
==============
BotInterbreedEndMatch

add link back into ExitLevel?
==============
 */
void BotInterbreedEndMatch(void) {

    if (!bot_interbreed) return;
    bot_interbreedmatchcount++;
    if (bot_interbreedmatchcount >= bot_interbreedcycle.integer) {
        bot_interbreedmatchcount = 0;
        //
        trap_Cvar_Update(&bot_interbreedwrite);
        if (strlen(bot_interbreedwrite.string)) {
            BotWriteInterbreeded(bot_interbreedwrite.string);
            trap_Cvar_Set("bot_interbreedwrite", "");
        }
        BotInterbreedBots();
    }
}

/*
==============
BotInterbreeding
==============
 */
void BotInterbreeding(void) {
    int i;

    trap_Cvar_Update(&bot_interbreedchar);
    if (!strlen(bot_interbreedchar.string)) return;
    //shutdown all the bots
    for (i = 0; i < MAX_CLIENTS; i++) {
        if (botstates[i] && botstates[i]->inuse) {
            BotAIShutdownClient(botstates[i]->client, qfalse);
        }
    }
    //make sure all item weight configs are reloaded and Not shared
    trap_BotLibVarSet("bot_reloadcharacters", "1");
    //add a number of bots using the desired bot character
    for (i = 0; i < bot_interbreedbots.integer; i++) {
        trap_SendConsoleCommand(EXEC_INSERT, va("addbot %s 4 free %i %s%d\n",
                bot_interbreedchar.string, i * 50, bot_interbreedchar.string, i));
    }
    //
    trap_Cvar_Set("bot_interbreedchar", "");
    bot_interbreed = qtrue;
}

/*
==============
BotEntityInfo
==============
 */
void BotEntityInfo(int entnum, aas_entityinfo_t *info) {
    trap_AAS_EntityInfo(entnum, info);
}

/*
==============
NumBots
==============
 */
int NumBots(void) {
    return numbots;
}

/*
==============
BotTeamLeader
==============
 */
int BotTeamLeader(bot_state_t *bs) {
    int leader;

    leader = ClientFromName(bs->teamleader);
    if (leader < 0) return qfalse;
    if (!botstates[leader] || !botstates[leader]->inuse) return qfalse;
    return qtrue;
}

/*
==============
AngleDifference
==============
 */
float AngleDifference(float ang1, float ang2) {
    float diff;

    diff = ang1 - ang2;
    if (ang1 > ang2) {
        if (diff > 180.0) diff -= 360.0;
    } else {
        if (diff < -180.0) diff += 360.0;
    }
    return diff;
}

/*
==============
BotChangeViewAngle
==============
 */
float BotChangeViewAngle(float angle, float ideal_angle, float speed) {
    float move;

    angle = AngleMod(angle);
    ideal_angle = AngleMod(ideal_angle);
    if (angle == ideal_angle) return angle;
    move = ideal_angle - angle;
    if (ideal_angle > angle) {
        if (move > 180.0) move -= 360.0;
    } else {
        if (move < -180.0) move += 360.0;
    }
    if (move > 0) {
        if (move > speed) move = speed;
    } else {
        if (move < -speed) move = -speed;
    }
    return AngleMod(angle + move);
}

/*
==============
BotChangeViewAngles
==============
 */
void BotChangeViewAngles(bot_state_t *bs, float thinktime) {
    float diff, factor, maxchange, anglespeed, disired_speed;
    int i;

    if (bs->ideal_viewangles[PITCH] > 180) bs->ideal_viewangles[PITCH] -= 360;
    //
    if (bs->enemy >= 0) {
        factor = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_VIEW_FACTOR, 0.01f, 1);
        maxchange = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_VIEW_MAXCHANGE, 1, 1800);
    } else {
        factor = 0.05f;
        maxchange = 360;
    }
    if (maxchange < 240) maxchange = 240;
    maxchange *= thinktime;
    for (i = 0; i < 2; i++) {
        //
        if (bot_challenge.integer) {
            //smooth slowdown view model
            diff = abs(AngleDifference(bs->viewangles[i], bs->ideal_viewangles[i]));
            anglespeed = diff * factor;
            if (anglespeed > maxchange) anglespeed = maxchange;
            bs->viewangles[i] = BotChangeViewAngle(bs->viewangles[i],
                    bs->ideal_viewangles[i], anglespeed);
        } else {
            //over reaction view model
            bs->viewangles[i] = AngleMod(bs->viewangles[i]);
            bs->ideal_viewangles[i] = AngleMod(bs->ideal_viewangles[i]);
            diff = AngleDifference(bs->viewangles[i], bs->ideal_viewangles[i]);
            disired_speed = diff * factor;
            bs->viewanglespeed[i] += (bs->viewanglespeed[i] - disired_speed);
            if (bs->viewanglespeed[i] > 180) bs->viewanglespeed[i] = maxchange;
            if (bs->viewanglespeed[i] < -180) bs->viewanglespeed[i] = -maxchange;
            anglespeed = bs->viewanglespeed[i];
            if (anglespeed > maxchange) anglespeed = maxchange;
            if (anglespeed < -maxchange) anglespeed = -maxchange;
            bs->viewangles[i] += anglespeed;
            bs->viewangles[i] = AngleMod(bs->viewangles[i]);
            //demping
            bs->viewanglespeed[i] *= 0.45 * (1 - factor);
        }
        //BotAI_Print(PRT_MESSAGE, "ideal_angles %f %f\n", bs->ideal_viewangles[0], bs->ideal_viewangles[1], bs->ideal_viewangles[2]);`
        //bs->viewangles[i] = bs->ideal_viewangles[i];
    }
    //bs->viewangles[PITCH] = 0;
    if (bs->viewangles[PITCH] > 180) bs->viewangles[PITCH] -= 360;
    //elementary action: view
    trap_EA_View(bs->client, bs->viewangles);
}

/*
==============
BotInputToUserCommand
==============
 */
void BotInputToUserCommand(bot_input_t *bi, usercmd_t *ucmd, int delta_angles[3], int time) {
    vec3_t angles, forward, right;
    short temp;
    int j;

    //clear the whole structure
    memset(ucmd, 0, sizeof (usercmd_t));
    //
    //Com_Printf("dir = %f %f %f speed = %f\n", bi->dir[0], bi->dir[1], bi->dir[2], bi->speed);
    //the duration for the user command in milli seconds
    ucmd->serverTime = time;
    //
    if (bi->actionflags & ACTION_DELAYEDJUMP) {
        bi->actionflags |= ACTION_JUMP;
        bi->actionflags &= ~ACTION_DELAYEDJUMP;
    }
    //set the buttons
    if (bi->actionflags & ACTION_RESPAWN) ucmd->buttons = BUTTON_ATTACK;
    if (bi->actionflags & ACTION_ATTACK) ucmd->buttons |= BUTTON_ATTACK;
    if (bi->actionflags & ACTION_TALK) ucmd->buttons |= BUTTON_TALK;
    if (bi->actionflags & ACTION_GESTURE) ucmd->buttons |= BUTTON_GESTURE;
    if (bi->actionflags & ACTION_USE) ucmd->buttons |= BUTTON_USE_HOLDABLE;
    if (bi->actionflags & ACTION_WALK) ucmd->buttons |= BUTTON_WALKING;
    //if (bi->actionflags & ACTION_WALK) ucmd->buttons |= BUTTON_SPRINT;
    /*if (bi->actionflags & ACTION_AFFIRMATIVE) ucmd->buttons |= BUTTON_AFFIRMATIVE;
    if (bi->actionflags & ACTION_NEGATIVE) ucmd->buttons |= BUTTON_NEGATIVE;
    if (bi->actionflags & ACTION_GETFLAG) ucmd->buttons |= BUTTON_GETFLAG;
    if (bi->actionflags & ACTION_GUARDBASE) ucmd->buttons |= BUTTON_GUARDBASE;
    if (bi->actionflags & ACTION_PATROL) ucmd->buttons |= BUTTON_PATROL;
    if (bi->actionflags & ACTION_FOLLOWME) ucmd->buttons |= BUTTON_FOLLOWME;*/
    //
    ucmd->weapon = bi->weapon;
    //set the view angles
    //NOTE: the ucmd->angles are the angles WITHOUT the delta angles
    ucmd->angles[PITCH] = ANGLE2SHORT(bi->viewangles[PITCH]);
    ucmd->angles[YAW] = ANGLE2SHORT(bi->viewangles[YAW]);
    ucmd->angles[ROLL] = ANGLE2SHORT(bi->viewangles[ROLL]);
    //subtract the delta angles
    for (j = 0; j < 3; j++) {
        temp = ucmd->angles[j] - delta_angles[j];
        /*NOTE: disabled because temp should be mod first
        if ( j == PITCH ) {
                // don't let the player look up or down more than 90 degrees
                if ( temp > 16000 ) temp = 16000;
                else if ( temp < -16000 ) temp = -16000;
        }
         */
        ucmd->angles[j] = temp;
    }
    //NOTE: movement is relative to the REAL view angles
    //get the horizontal forward and right vector
    //get the pitch in the range [-180, 180]
    if (bi->dir[2]) angles[PITCH] = bi->viewangles[PITCH];
    else angles[PITCH] = 0;
    angles[YAW] = bi->viewangles[YAW];
    angles[ROLL] = 0;
    AngleVectors(angles, forward, right, NULL);
    //bot input speed is in the range [0, 400]
    bi->speed = bi->speed * 127 / 400;
    //set the view independent movement
    ucmd->forwardmove = DotProduct(forward, bi->dir) * bi->speed;
    ucmd->rightmove = DotProduct(right, bi->dir) * bi->speed;
    ucmd->upmove = abs(forward[2]) * bi->dir[2] * bi->speed;
    //normal keyboard movement
    if (bi->actionflags & ACTION_MOVEFORWARD) ucmd->forwardmove += 127;
    if (bi->actionflags & ACTION_MOVEBACK) ucmd->forwardmove -= 127;
    if (bi->actionflags & ACTION_MOVELEFT) ucmd->rightmove -= 127;
    if (bi->actionflags & ACTION_MOVERIGHT) ucmd->rightmove += 127;
    //jump/moveup
    if (bi->actionflags & ACTION_JUMP) ucmd->upmove += 127;
    //crouch/movedown
    if (bi->actionflags & ACTION_CROUCH) ucmd->upmove -= 127;
    //
    //Com_Printf("forward = %d right = %d up = %d\n", ucmd.forwardmove, ucmd.rightmove, ucmd.upmove);
    //Com_Printf("ucmd->serverTime = %d\n", ucmd->serverTime);
}

/*
==============
BotUpdateInput
==============
 */
void BotUpdateInput(bot_state_t *bs, int time, int elapsed_time) {
    bot_input_t bi;
    int j;

    //add the delta angles to the bot's current view angles
    for (j = 0; j < 3; j++) {
        bs->viewangles[j] = AngleMod(bs->viewangles[j] + SHORT2ANGLE(bs->cur_ps.delta_angles[j]));
    }
    //change the bot view angles
    BotChangeViewAngles(bs, (float) elapsed_time / 1000);
    //retrieve the bot input
    trap_EA_GetInput(bs->client, (float) time / 1000, &bi);
    //respawn hack
    if (bi.actionflags & ACTION_RESPAWN) {
        if (bs->lastucmd.buttons & BUTTON_ATTACK) bi.actionflags &= ~(ACTION_RESPAWN | ACTION_ATTACK);
    }
    //convert the bot input to a usercmd
    BotInputToUserCommand(&bi, &bs->lastucmd, bs->cur_ps.delta_angles, time);
    //subtract the delta angles
    for (j = 0; j < 3; j++) {
        bs->viewangles[j] = AngleMod(bs->viewangles[j] - SHORT2ANGLE(bs->cur_ps.delta_angles[j]));
    }
}

/*
==============
BotAIRegularUpdate
==============
 */
void BotAIRegularUpdate(void) {
    if (regularupdate_time < FloatTime()) {
        trap_BotUpdateEntityItems();
        regularupdate_time = FloatTime() + 0.3;
    }
}

/*
==============
RemoveColorEscapeSequences
==============
 */
void RemoveColorEscapeSequences(char *text) {
    int i, l;

    l = 0;
    for (i = 0; text[i]; i++) {
        if (Q_IsColorString(&text[i])) {
            i++;
            continue;
        }
        if (text[i] > 0x7E)
            continue;
        text[l++] = text[i];
    }
    text[l] = '\0';
}

/*
==============
BotAI
==============
 */
int BotAI(int client, float thinktime) {
    bot_state_t *bs;
    char buf[1024], *args;
    int j;

    trap_EA_ResetInput(client);
    //
    bs = botstates[client];
    if (!bs || !bs->inuse) {
        BotAI_Print(PRT_FATAL, "BotAI: client %d is not setup\n", client);
        return qfalse;
    }

    //retrieve the current client state
    BotAI_GetClientState(client, &bs->cur_ps);

    //retrieve any waiting server commands
    while (trap_BotGetServerCommand(client, buf, sizeof (buf))) {
        //have buf point to the command and args to the command arguments
        args = strchr(buf, ' ');
        if (!args) continue;
        *args++ = '\0';

        //remove color espace sequences from the arguments
        RemoveColorEscapeSequences(args);

        if (!Q_stricmp(buf, "cp ")) {
            /*CenterPrintf*/
        } else if (!Q_stricmp(buf, "cs")) {
            /*ConfigStringModified*/
        } else if (!Q_stricmp(buf, "print")) {
            //remove first and last quote from the chat message
            memmove(args, args + 1, strlen(args));
            args[strlen(args) - 1] = '\0';
            trap_BotQueueConsoleMessage(bs->cs, CMS_NORMAL, args);
        } else if (!Q_stricmp(buf, "chat")) {
            //remove first and last quote from the chat message
            memmove(args, args + 1, strlen(args));
            args[strlen(args) - 1] = '\0';
            trap_BotQueueConsoleMessage(bs->cs, CMS_CHAT, args);
        } else if (!Q_stricmp(buf, "tchat")) {
            //remove first and last quote from the chat message
            memmove(args, args + 1, strlen(args));
            args[strlen(args) - 1] = '\0';
            trap_BotQueueConsoleMessage(bs->cs, CMS_CHAT, args);
        }
#ifdef MISSIONPACK
        else if (!Q_stricmp(buf, "vchat")) {
            BotVoiceChatCommand(bs, SAY_ALL, args);
        } else if (!Q_stricmp(buf, "vtchat")) {
            BotVoiceChatCommand(bs, SAY_TEAM, args);
        } else if (!Q_stricmp(buf, "vtell")) {
            BotVoiceChatCommand(bs, SAY_TELL, args);
        }
#endif
        else if (!Q_stricmp(buf, "scores")) {
            /*FIXME: parse scores?*/
        } else if (!Q_stricmp(buf, "clientLevelShot")) {
            /*ignore*/
        }
    }
    //add the delta angles to the bot's current view angles
    for (j = 0; j < 3; j++) {
        bs->viewangles[j] = AngleMod(bs->viewangles[j] + SHORT2ANGLE(bs->cur_ps.delta_angles[j]));
    }
    //increase the local time of the bot
    bs->ltime += thinktime;
    //
    bs->thinktime = thinktime;
    //origin of the bot
    VectorCopy(bs->cur_ps.origin, bs->origin);
    //eye coordinates of the bot
    VectorCopy(bs->cur_ps.origin, bs->eye);
    bs->eye[2] += bs->cur_ps.viewheight;
    //get the area the bot is in
    bs->areanum = BotPointAreaNum(bs->origin);
    //the real AI
    BotDeathmatchAI(bs, thinktime);
    //set the weapon selection every AI frame
    trap_EA_SelectWeapon(bs->client, bs->weaponnum);
    //subtract the delta angles
    for (j = 0; j < 3; j++) {
        bs->viewangles[j] = AngleMod(bs->viewangles[j] - SHORT2ANGLE(bs->cur_ps.delta_angles[j]));
    }
    //everything was ok
    return qtrue;
}

/*
==================
BotScheduleBotThink
==================
 */
void BotScheduleBotThink(void) {
    int i, botnum;

    botnum = 0;

    for (i = 0; i < MAX_CLIENTS; i++) {
        if (!botstates[i] || !botstates[i]->inuse) {
            continue;
        }
        //initialize the bot think residual time
        botstates[i]->botthink_residual = bot_thinktime.integer * botnum / numbots;
        botnum++;
    }
}

/*
==============
BotWriteSessionData
==============
 */
void BotWriteSessionData(bot_state_t *bs) {
    const char *s;
    const char *var;

    s = va(
            "%i %i %i %i %i %i %i %i"
            " %f %f %f"
            " %f %f %f"
            " %f %f %f",
            bs->lastgoal_decisionmaker,
            bs->lastgoal_ltgtype,
            bs->lastgoal_teammate,
            bs->lastgoal_teamgoal.areanum,
            bs->lastgoal_teamgoal.entitynum,
            bs->lastgoal_teamgoal.flags,
            bs->lastgoal_teamgoal.iteminfo,
            bs->lastgoal_teamgoal.number,
            bs->lastgoal_teamgoal.origin[0],
            bs->lastgoal_teamgoal.origin[1],
            bs->lastgoal_teamgoal.origin[2],
            bs->lastgoal_teamgoal.mins[0],
            bs->lastgoal_teamgoal.mins[1],
            bs->lastgoal_teamgoal.mins[2],
            bs->lastgoal_teamgoal.maxs[0],
            bs->lastgoal_teamgoal.maxs[1],
            bs->lastgoal_teamgoal.maxs[2]
            );

    var = va("botsession%i", bs->client);

    trap_Cvar_Set(var, s);
}

/*
==============
BotReadSessionData
==============
 */
void BotReadSessionData(bot_state_t *bs) {
    char s[MAX_STRING_CHARS];
    const char *var;

    var = va("botsession%i", bs->client);
    trap_Cvar_VariableStringBuffer(var, s, sizeof (s));

    sscanf(s,
            "%i %i %i %i %i %i %i %i"
            " %f %f %f"
            " %f %f %f"
            " %f %f %f",
            &bs->lastgoal_decisionmaker,
            &bs->lastgoal_ltgtype,
            &bs->lastgoal_teammate,
            &bs->lastgoal_teamgoal.areanum,
            &bs->lastgoal_teamgoal.entitynum,
            &bs->lastgoal_teamgoal.flags,
            &bs->lastgoal_teamgoal.iteminfo,
            &bs->lastgoal_teamgoal.number,
            &bs->lastgoal_teamgoal.origin[0],
            &bs->lastgoal_teamgoal.origin[1],
            &bs->lastgoal_teamgoal.origin[2],
            &bs->lastgoal_teamgoal.mins[0],
            &bs->lastgoal_teamgoal.mins[1],
            &bs->lastgoal_teamgoal.mins[2],
            &bs->lastgoal_teamgoal.maxs[0],
            &bs->lastgoal_teamgoal.maxs[1],
            &bs->lastgoal_teamgoal.maxs[2]
            );
}

/*
==============
BotAISetupClient
==============
 */
int BotAISetupClient(int client, struct bot_settings_s *settings, qboolean restart) {
    char filename[MAX_PATH], name[MAX_PATH], gender[MAX_PATH];
    bot_state_t *bs;
    int errnum;

    if (!botstates[client]) botstates[client] = G_Alloc(sizeof (bot_state_t));
    bs = botstates[client];

    if (bs && bs->inuse) {
        BotAI_Print(PRT_FATAL, "BotAISetupClient: client %d already setup\n", client);
        return qfalse;
    }

    if (!trap_AAS_Initialized()) {
        BotAI_Print(PRT_FATAL, "AAS not initialized\n");
        return qfalse;
    }

    //load the bot character
    bs->character = trap_BotLoadCharacter(settings->characterfile, settings->skill);
    if (!bs->character) {
        BotAI_Print(PRT_FATAL, "couldn't load skill %f from %s\n", settings->skill, settings->characterfile);
        return qfalse;
    }
    //copy the settings
    memcpy(&bs->settings, settings, sizeof (bot_settings_t));
    //allocate a goal state
    bs->gs = trap_BotAllocGoalState(client);
    //load the item weights
    trap_Characteristic_String(bs->character, CHARACTERISTIC_ITEMWEIGHTS, filename, MAX_PATH);
    errnum = trap_BotLoadItemWeights(bs->gs, filename);
    if (errnum != BLERR_NOERROR) {
        trap_BotFreeGoalState(bs->gs);
        return qfalse;
    }
    //allocate a weapon state
    bs->ws = trap_BotAllocWeaponState();
    //load the weapon weights
    trap_Characteristic_String(bs->character, CHARACTERISTIC_WEAPONWEIGHTS, filename, MAX_PATH);
    errnum = trap_BotLoadWeaponWeights(bs->ws, filename);
    if (errnum != BLERR_NOERROR) {
        trap_BotFreeGoalState(bs->gs);
        trap_BotFreeWeaponState(bs->ws);
        return qfalse;
    }
    //allocate a chat state
    bs->cs = trap_BotAllocChatState();
    //load the chat file
    trap_Characteristic_String(bs->character, CHARACTERISTIC_CHAT_FILE, filename, MAX_PATH);
    trap_Characteristic_String(bs->character, CHARACTERISTIC_CHAT_NAME, name, MAX_PATH);
    errnum = trap_BotLoadChatFile(bs->cs, filename, name);
    if (errnum != BLERR_NOERROR) {
        trap_BotFreeChatState(bs->cs);
        trap_BotFreeGoalState(bs->gs);
        trap_BotFreeWeaponState(bs->ws);
        return qfalse;
    }
    //get the gender characteristic
    trap_Characteristic_String(bs->character, CHARACTERISTIC_GENDER, gender, MAX_PATH);
    //set the chat gender
    if (*gender == 'f' || *gender == 'F') trap_BotSetChatGender(bs->cs, CHAT_GENDERFEMALE);
    else if (*gender == 'm' || *gender == 'M') trap_BotSetChatGender(bs->cs, CHAT_GENDERMALE);
    else trap_BotSetChatGender(bs->cs, CHAT_GENDERLESS);

    bs->inuse = qtrue;
    bs->client = client;
    bs->entitynum = client;
    bs->setupcount = 4;
    bs->entergame_time = FloatTime();
    bs->ms = trap_BotAllocMoveState();
    bs->walker = trap_Characteristic_BFloat(bs->character, CHARACTERISTIC_WALKER, 0, 1);
    numbots++;

    if (trap_Cvar_VariableIntegerValue("bot_testichat")) {
        trap_BotLibVarSet("bot_testichat", "1");
        BotChatTest(bs);
    }
    //NOTE: reschedule the bot thinking
    BotScheduleBotThink();
    //if interbreeding start with a mutation
    if (bot_interbreed) {
        trap_BotMutateGoalFuzzyLogic(bs->gs, 1);
    }
    // if we kept the bot client
    if (restart) {
        BotReadSessionData(bs);
    }
    //bot has been setup succesfully
    return qtrue;
}

/*
==============
BotAIShutdownClient
==============
 */
int BotAIShutdownClient(int client, qboolean restart) {
    bot_state_t *bs;

    bs = botstates[client];
    if (!bs || !bs->inuse) {
        //BotAI_Print(PRT_ERROR, "BotAIShutdownClient: client %d already shutdown\n", client);
        return qfalse;
    }

    if (restart) {
        BotWriteSessionData(bs);
    }

    if (BotChat_ExitGame(bs)) {
        trap_BotEnterChat(bs->cs, bs->client, CHAT_ALL);
    }

    trap_BotFreeMoveState(bs->ms);
    //free the goal state`
    trap_BotFreeGoalState(bs->gs);
    //free the chat file
    trap_BotFreeChatState(bs->cs);
    //free the weapon weights
    trap_BotFreeWeaponState(bs->ws);
    //free the bot character
    trap_BotFreeCharacter(bs->character);
    //
    BotFreeWaypoints(bs->checkpoints);
    BotFreeWaypoints(bs->patrolpoints);
    //clear activate goal stack
    BotClearActivateGoalStack(bs);
    //clear the bot state
    memset(bs, 0, sizeof (bot_state_t));
    //set the inuse flag to qfalse
    bs->inuse = qfalse;
    //there's one bot less
    numbots--;
    //everything went ok
    return qtrue;
}

/*
==============
BotResetState

called when a bot enters the intermission or observer mode and
when the level is changed
==============
 */
void BotResetState(bot_state_t *bs) {
    int client, entitynum, inuse;
    int movestate, goalstate, chatstate, weaponstate;
    bot_settings_t settings;
    int character;
    playerState_t ps; //current player state
    float entergame_time;

    //save some things that should not be reset here
    memcpy(&settings, &bs->settings, sizeof (bot_settings_t));
    memcpy(&ps, &bs->cur_ps, sizeof (playerState_t));
    inuse = bs->inuse;
    client = bs->client;
    entitynum = bs->entitynum;
    character = bs->character;
    movestate = bs->ms;
    goalstate = bs->gs;
    chatstate = bs->cs;
    weaponstate = bs->ws;
    entergame_time = bs->entergame_time;
    //free checkpoints and patrol points
    BotFreeWaypoints(bs->checkpoints);
    BotFreeWaypoints(bs->patrolpoints);
    //reset the whole state
    memset(bs, 0, sizeof (bot_state_t));
    //copy back some state stuff that should not be reset
    bs->ms = movestate;
    bs->gs = goalstate;
    bs->cs = chatstate;
    bs->ws = weaponstate;
    memcpy(&bs->cur_ps, &ps, sizeof (playerState_t));
    memcpy(&bs->settings, &settings, sizeof (bot_settings_t));
    bs->inuse = inuse;
    bs->client = client;
    bs->entitynum = entitynum;
    bs->character = character;
    bs->entergame_time = entergame_time;
    //reset several states
    if (bs->ms) trap_BotResetMoveState(bs->ms);
    if (bs->gs) trap_BotResetGoalState(bs->gs);
    if (bs->ws) trap_BotResetWeaponState(bs->ws);
    if (bs->gs) trap_BotResetAvoidGoals(bs->gs);
    if (bs->ms) trap_BotResetAvoidReach(bs->ms);
}

/*
==============
BotAILoadMap
==============
 */
int BotAILoadMap(int restart) {
    int i;
    vmCvar_t mapname;

    if (!restart) {
        trap_Cvar_Register(&mapname, "mapname", "", CVAR_SERVERINFO | CVAR_ROM);
        trap_BotLibLoadMap(mapname.string);
    }

    for (i = 0; i < MAX_CLIENTS; i++) {
        if (botstates[i] && botstates[i]->inuse) {
            BotResetState(botstates[i]);
            botstates[i]->setupcount = 4;
        }
    }

    BotSetupDeathmatchAI();

    return qtrue;
}

/*
==================
BotAIStartFrame
==================
 */
int BotAIStartFrame(int time) {
    int i;
    gentity_t *ent;
    bot_entitystate_t state;
    int elapsed_time, thinktime;
    static int local_time;
    static int botlib_residual;
    static int lastbotthink_time;

    G_CheckBotSpawn();

    trap_Cvar_Update(&bot_rocketjump);
    trap_Cvar_Update(&bot_grapple);
    trap_Cvar_Update(&bot_fastchat);
    trap_Cvar_Update(&bot_nochat);
    trap_Cvar_Update(&bot_testrchat);
    trap_Cvar_Update(&bot_thinktime);
    trap_Cvar_Update(&bot_memorydump);
    trap_Cvar_Update(&bot_saveroutingcache);
    trap_Cvar_Update(&bot_pause);
    trap_Cvar_Update(&bot_report);

    if (bot_report.integer) {
        //		BotTeamplayReport();
        //		trap_Cvar_Set("bot_report", "0");
        BotUpdateInfoConfigStrings();
    }

    if (bot_pause.integer) {
        // execute bot user commands every frame
        for (i = 0; i < MAX_CLIENTS; i++) {
            if (!botstates[i] || !botstates[i]->inuse) {
                continue;
            }
            if (g_entities[i].client->pers.connected != CON_CONNECTED) {
                continue;
            }
            botstates[i]->lastucmd.forwardmove = 0;
            botstates[i]->lastucmd.rightmove = 0;
            botstates[i]->lastucmd.upmove = 0;
            botstates[i]->lastucmd.buttons = 0;
            botstates[i]->lastucmd.serverTime = time;
            trap_BotUserCommand(botstates[i]->client, &botstates[i]->lastucmd);
        }
        return qtrue;
    }

    if (bot_memorydump.integer) {
        trap_BotLibVarSet("memorydump", "1");
        trap_Cvar_Set("bot_memorydump", "0");
    }
    if (bot_saveroutingcache.integer) {
        trap_BotLibVarSet("saveroutingcache", "1");
        trap_Cvar_Set("bot_saveroutingcache", "0");
    }
    //check if bot interbreeding is activated
    BotInterbreeding();
    //cap the bot think time
    if (bot_thinktime.integer > 200) {
        trap_Cvar_Set("bot_thinktime", "200");
    }
    //if the bot think time changed we should reschedule the bots
    if (bot_thinktime.integer != lastbotthink_time) {
        lastbotthink_time = bot_thinktime.integer;
        BotScheduleBotThink();
    }

    elapsed_time = time - local_time;
    local_time = time;

    botlib_residual += elapsed_time;

    if (elapsed_time > bot_thinktime.integer) thinktime = elapsed_time;
    else thinktime = bot_thinktime.integer;

    // update the bot library
    if (botlib_residual >= thinktime) {
        botlib_residual -= thinktime;

        trap_BotLibStartFrame((float) time / 1000);

        if (!trap_AAS_Initialized()) return qfalse;

        //update entities in the botlib
        for (i = 0; i < MAX_GENTITIES; i++) {
            ent = &g_entities[i];
            if (!ent->inuse) {
                trap_BotLibUpdateEntity(i, NULL);
                continue;
            }
            if (!ent->r.linked) {
                trap_BotLibUpdateEntity(i, NULL);
                continue;
            }
            if (ent->r.svFlags & SVF_NOCLIENT) {
                trap_BotLibUpdateEntity(i, NULL);
                continue;
            }
            // do not update missiles
            if (ent->s.eType == ET_MISSILE) {
                trap_BotLibUpdateEntity(i, NULL);
                continue;
            }
            // do not update event only entities
            if (ent->s.eType > ET_EVENTS) {
                trap_BotLibUpdateEntity(i, NULL);
                continue;
            }
            //
            memset(&state, 0, sizeof (bot_entitystate_t));
            //
            VectorCopy(ent->r.currentOrigin, state.origin);
            if (i < MAX_CLIENTS) {
                VectorCopy(ent->s.apos.trBase, state.angles);
            } else {
                VectorCopy(ent->r.currentAngles, state.angles);
            }
            VectorCopy(ent->s.origin2, state.old_origin);
            VectorCopy(ent->r.mins, state.mins);
            VectorCopy(ent->r.maxs, state.maxs);
            state.type = ent->s.eType;
            state.flags = ent->s.eFlags;
            if (ent->r.bmodel) state.solid = SOLID_BSP;
            else state.solid = SOLID_BBOX;
            state.groundent = ent->s.groundEntityNum;
            state.modelindex = ent->s.modelindex;
            state.modelindex2 = ent->s.modelindex2;
            state.frame = ent->s.frame;
            state.event = ent->s.event;
            state.eventParm = ent->s.eventParm;
            state.powerups = ent->s.powerups;
            state.legsAnim = ent->s.legsAnim;
            state.torsoAnim = ent->s.torsoAnim;
            state.weapon = ent->s.weapon;
            //
            trap_BotLibUpdateEntity(i, &state);
        }

        BotAIRegularUpdate();
    }

    floattime = trap_AAS_Time();

    // execute scheduled bot AI
    for (i = 0; i < MAX_CLIENTS; i++) {
        if (!botstates[i] || !botstates[i]->inuse) {
            continue;
        }
        //
        botstates[i]->botthink_residual += elapsed_time;
        //
        if (botstates[i]->botthink_residual >= thinktime) {
            botstates[i]->botthink_residual -= thinktime;

            if (!trap_AAS_Initialized()) return qfalse;

            if (g_entities[i].client->pers.connected == CON_CONNECTED) {
                BotAI(i, (float) thinktime / 1000);
            }
        }
    }


    // execute bot user commands every frame
    for (i = 0; i < MAX_CLIENTS; i++) {
        if (!botstates[i] || !botstates[i]->inuse) {
            continue;
        }
        if (g_entities[i].client->pers.connected != CON_CONNECTED) {
            continue;
        }

        BotUpdateInput(botstates[i], time, elapsed_time);
        trap_BotUserCommand(botstates[i]->client, &botstates[i]->lastucmd);
    }

    return qtrue;
}

/*
==============
BotInitLibrary
==============
 */
int BotInitLibrary(void) {
    char buf[144];

    //set the maxclients and maxentities library variables before calling BotSetupLibrary
    trap_Cvar_VariableStringBuffer("sv_maxclients", buf, sizeof (buf));
    if (!strlen(buf)) strcpy(buf, "8");
    trap_BotLibVarSet("maxclients", buf);
    Com_sprintf(buf, sizeof (buf), "%d", MAX_GENTITIES);
    trap_BotLibVarSet("maxentities", buf);
    //bsp checksum
    trap_Cvar_VariableStringBuffer("sv_mapChecksum", buf, sizeof (buf));
    if (strlen(buf)) trap_BotLibVarSet("sv_mapChecksum", buf);
    //maximum number of aas links
    trap_Cvar_VariableStringBuffer("max_aaslinks", buf, sizeof (buf));
    if (strlen(buf)) trap_BotLibVarSet("max_aaslinks", buf);
    //maximum number of items in a level
    trap_Cvar_VariableStringBuffer("max_levelitems", buf, sizeof (buf));
    if (strlen(buf)) trap_BotLibVarSet("max_levelitems", buf);
    //game type
    trap_Cvar_VariableStringBuffer("g_gametype", buf, sizeof (buf));
    if (!strlen(buf)) strcpy(buf, "0");
    trap_BotLibVarSet("g_gametype", buf);
    //bot developer mode and log file
    trap_BotLibVarSet("bot_developer", bot_developer.string);
    trap_Cvar_VariableStringBuffer("logfile", buf, sizeof (buf));
    trap_BotLibVarSet("log", buf);
    //no chatting
    trap_Cvar_VariableStringBuffer("bot_nochat", buf, sizeof (buf));
    if (strlen(buf)) trap_BotLibVarSet("nochat", buf);
    //visualize jump pads
    trap_Cvar_VariableStringBuffer("bot_visualizejumppads", buf, sizeof (buf));
    if (strlen(buf)) trap_BotLibVarSet("bot_visualizejumppads", buf);
    //forced clustering calculations
    trap_Cvar_VariableStringBuffer("bot_forceclustering", buf, sizeof (buf));
    if (strlen(buf)) trap_BotLibVarSet("forceclustering", buf);
    //forced reachability calculations
    trap_Cvar_VariableStringBuffer("bot_forcereachability", buf, sizeof (buf));
    if (strlen(buf)) trap_BotLibVarSet("forcereachability", buf);
    //force writing of AAS to file
    trap_Cvar_VariableStringBuffer("bot_forcewrite", buf, sizeof (buf));
    if (strlen(buf)) trap_BotLibVarSet("forcewrite", buf);
    //no AAS optimization
    trap_Cvar_VariableStringBuffer("bot_aasoptimize", buf, sizeof (buf));
    if (strlen(buf)) trap_BotLibVarSet("aasoptimize", buf);
    //
    trap_Cvar_VariableStringBuffer("bot_saveroutingcache", buf, sizeof (buf));
    if (strlen(buf)) trap_BotLibVarSet("saveroutingcache", buf);
    //reload instead of cache bot character files
    trap_Cvar_VariableStringBuffer("bot_reloadcharacters", buf, sizeof (buf));
    if (!strlen(buf)) strcpy(buf, "0");
    trap_BotLibVarSet("bot_reloadcharacters", buf);
    //base directory
    trap_Cvar_VariableStringBuffer("fs_basepath", buf, sizeof (buf));
    if (strlen(buf)) trap_BotLibVarSet("basedir", buf);
    //game directory
    trap_Cvar_VariableStringBuffer("fs_game", buf, sizeof (buf));
    if (strlen(buf)) trap_BotLibVarSet("gamedir", buf);
    //home directory
    trap_Cvar_VariableStringBuffer("fs_homepath", buf, sizeof (buf));
    if (strlen(buf)) trap_BotLibVarSet("homedir", buf);
    //
#ifdef MISSIONPACK
    trap_BotLibDefine("MISSIONPACK");
#endif
    //setup the bot library
    return trap_BotLibSetup();
}

/*
==============
BotAISetup
==============
 */
int BotAISetup(int restart) {
    int errnum;

    trap_Cvar_Register(&bot_thinktime, "bot_thinktime", "100", CVAR_CHEAT);
    trap_Cvar_Register(&bot_memorydump, "bot_memorydump", "0", CVAR_CHEAT);
    trap_Cvar_Register(&bot_saveroutingcache, "bot_saveroutingcache", "0", CVAR_CHEAT);
    trap_Cvar_Register(&bot_pause, "bot_pause", "0", CVAR_CHEAT);
    trap_Cvar_Register(&bot_report, "bot_report", "0", CVAR_CHEAT);
    trap_Cvar_Register(&bot_testsolid, "bot_testsolid", "0", CVAR_CHEAT);
    trap_Cvar_Register(&bot_testclusters, "bot_testclusters", "0", CVAR_CHEAT);
    trap_Cvar_Register(&bot_developer, "bot_developer", "0", CVAR_CHEAT);
    trap_Cvar_Register(&bot_interbreedchar, "bot_interbreedchar", "", 0);
    trap_Cvar_Register(&bot_interbreedbots, "bot_interbreedbots", "10", 0);
    trap_Cvar_Register(&bot_interbreedcycle, "bot_interbreedcycle", "20", 0);
    trap_Cvar_Register(&bot_interbreedwrite, "bot_interbreedwrite", "", 0);

    //if the game is restarted for a tournament
    if (restart) {
        return qtrue;
    }

    //initialize the bot states
    memset(botstates, 0, sizeof (botstates));

    errnum = BotInitLibrary();
    if (errnum != BLERR_NOERROR) return qfalse;
    return qtrue;
}

/*
==============
BotAIShutdown
==============
 */
int BotAIShutdown(int restart) {

    int i;

    //if the game is restarted for a tournament
    if (restart) {
        //shutdown all the bots in the botlib
        for (i = 0; i < MAX_CLIENTS; i++) {
            if (botstates[i] && botstates[i]->inuse) {
                BotAIShutdownClient(botstates[i]->client, restart);
            }
        }
        //don't shutdown the bot library
    } else {
        trap_BotLibShutdown();
    }
    return qtrue;
}

